package jmpo.strm;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * Потоки данных допускают следующие терминальные (конечные) операции:
 *     count()       - подсчитывает количество элементов
 *     min() / max() - минимальные и максимальный элементы
 *     allMatch()    - проверяет все элементы на соответствие уловиям
 *     anyMatch()    - хотя бы один соответствует уловию
 *     noneMatch()   - ни один элемент не удовлетворяет условию
 *     findAny()     - возвращает любой элемент и закрывает поток, если элемента нет - Optional.empty()
 *     findFirst()   - возвращает первый по порядку элемент, если порядок определён, и закрывает поток 
 *     если поток пуст - Optional.empty()
 *     reduce()      - сворачивает поток с помощью заданной операции. Сворачивание происходит в произвольном порядке
 *     toArray()     - создаёт массив из объектов потока данных
 *     forEach()     - применяет к каждому элементу заданную операцию
 *     forEachOrdered() - применяет к каждому элементу заданную операцию по порядку
 *     collect()     - метод для изменяющей редукции, потребляет элементы с возможностью изменять состояние внешних ресурсов
 */

/**
 * Далее рассмотрим подробнее мето reduce.
 * У метода reduce есть три перегруженных варианта:
 *     1) Optional<T> reduce(BinaryOperator<T> accumulator)
 *     2) T reduce(T identity, BinaryOperator<T> accumulator)
 *     3) U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
 * 
 * Метод reduce применяет операцию accumulator многократно к различным парам элементов. 
 * Например, взяв в качестве accumulator выражение (t1, t2) -> t1 + t2 мы можем применить редукцию к набору чисел:
 * {1, 2, 3, 4, 5, 6, 7} -> {(1, 2), 3, 4, 5, 6, 7} -> {3, 3, 4, 5, 6, 7} -> {(3, 3), 4, 5, 6, 7} ->
 * {6, 4, 5, 6, 7} -> {(6, 4), 5, 6, 7} -> {10, 5, 6, 7} -> ... -> {28}
 * 
 * В процессе работы процедура может применять одновременно над разными частями потока:
 * {1, 2, 3, 4, 5, 6, 7} -> {(1, 2), (3, 4), (5, 6), 7} -> {3, 7, 11 , 7} -> {(3,7), (11, 7)} ->
 * {10, 18} -> {(10,18)} -> {28}
 * 
 * Операция, которая может быть применена в любом порядке, но без переставления элементов, называется 
 * ассоциативной. reduce допускает только ассоциативные операции. 
 * 
 * Пример не ассоциативной операции, которая может дать непредсказуемый результат: (t1, t2) -> t1 - t2
 * {1, 2, 3, 4} -> {(1, 2), 3, 4} -> {-1, 3, 4} -> {(-1, 3), 4} -> {-4, 4} -> {(-4, 4)} -> {-8}
 * {1, 2, 3, 4} -> {1, 2, (3, 4)} -> {1, 2, -1} -> {1, (2, -1)} -> {1, 3} -> {(1, 3)} -> {-2}
 * 
 * Варианты (1) и (2) отличаются тем, что (2) принимает дополнительный элемент, который также сворачивается
 * с использованием accumulator. Если (1) вынуждена вернуть Optional.empty() на пустой коллекции, то (2) 
 * просто вернёт identity. Для элемента identity часто применяется свойство: для любого t из T верно 
 * accumulator(identity, t).equals(t), но невыполнение этого свойства не сломает метод reduce, однако результаты 
 * работы (1) и (2) на одной и той же коллекции с одинаковым accumulator будут отличаться.
 * 
 * Вариант (3) болле сложный. Здесь accumulator принимает два аргумента разных типов U и T.
 * В этом варианте каждый элемент обязательно сворачивается либо с identity, либо с последующим результатом вызова
 * accumulator. U всегда является левым аргументом. Так как выполнение может происходить параллельно, то неизбежно
 * возникнет ситуация, когда нам нужно будет скомбинировать два элемента U, за это отвечает третий параметр, причём
 * задаваемая им операция также должна быть ассоциативна.
 * Для U identity можно сформулировать такое правило: для любого u из U верно combiner(identity,u).equals(u).
 */

 /**
  * Контатинация слов через запятую с помощью reduce.
  */
final class ConcatReduceExample {
    public static void test() {
        List<String> lst = List.of("Get", "Set", "Let", "Bad", "Cat", "Rat");
        String result = lst.stream().parallel().reduce((s1, s2) -> s1 + ", " + s2).get();
        System.out.println(result);
    }
}

/**
 * Слово из первых букв строк с помощью reduce (3).
 */
final class AcronymExample {
    public static void test() {
        List<String> lst = List.of("Ann", "Rod", "Colin", "Orban", "Sam");
        String acronym = lst.stream().parallel()
            .reduce(
                "", 
                (s1, s2) -> s1 + s2.substring(0,1), 
                (s1, s2) -> s1 + s2);
        System.out.println(acronym);
    }
}

/**
 * Длина самой длинной строки reduce (3)
 */
final class MaxLengthExample {
    public static void test() {
        List<String> lst = List.of("aaa", "a", "ada", "ababa", "aaddaa", "bb", "bbb", "bbbbb");
        var maxlenght = lst.stream().parallel()
            .reduce(
                Integer.valueOf(0), 
                (l, s) -> Math.max(l, s.length()), 
                (l1, l2) -> Math.max(l1, l2));
        System.out.println(maxlenght);
    }
}

/**
 * Во многих случаях reduce (3) можно заменить на map с последующим reduce (1) или (2).
 * Длина самой длинной строки map->reduce.
 */
final class MaxLengthMapReduceExample {
    public static final void test() {
        List<String> lst = List.of("aaa", "a", "ada", "ababa", "aaddaa", "bb", "bbb", "bbbbb");
        var maxlenght = lst.stream().parallel()
            .map(s -> s.length())
            .reduce(
                Integer.valueOf(0), 
                (l1, l2) -> Math.max(l1, l2));
        System.out.println(maxlenght);
    }
}

/**
 * Рассмотрим подробнее метод collect.
 * У метода collect есть две реализации:
 *     1) R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
 *     2) R collect(Collector<? super T,A,R> collector)
 *    
 * Вариант (1) очень похож на reduce (3), часто метод collect даже называют mutable reduce.
 *     supplier реализует интерфейс Supplier<R> с единственным методом R get(). supplier используется в 
 *     потоке, чтобы создавать новые identity элементы, в частности пустых коллекций, в которые затем будет 
 *     происходить сбор элементов.
 * 
 *     accumulator как и BiFunction из reduce комбинирует слева коллекцию и справа элемент, возвращая новую 
 *     коллекцию.
 * 
 *     combiner как и в случае reduce используется для комбинирования двух независимых результатов из разных 
 *     потоков.
 * 
 * Разница с reduce состоит в том, что BiConsumer содержит метод accept(R r, T t), который ничего не возвращает 
 * и производит операцию комбинирования за счёт побочного эффекта. Также BiConsumer содержит дополнительный метод 
 * andThen(BiConsumer<? super R, ? super T> after), который должен комбинировать текущий combiner с after.
 * 
 * Возвращает collect результирующий объект R, который получился после всех combiner.
 */

/**
 * Пусть нам необходимо заполнить список удвоенными строками "aa" -> "aaaa"; "ab" -> "abab".
 * Выполним эту задачу с помощью collect.
 */
final class ArrayListCollectExample {
    public static void test() {
        List<String> lst = List.of("a", "ab", "bb", "ba", "cab", "dab");
        List<String> doubled = lst.stream().parallel().map(s -> s + s)
            .collect(
                () -> new ArrayList<String>(),
                (list, string) -> list.add(string),
                (listA, listB) -> listA.addAll(listB)    
            );
        for (var s : doubled)
            System.out.println(s);
    }
}

/**
 * С использованием ссылок на методы код может получиться короче.
 * Допустим нам нужно собрать в Set строки не длиннее 2.
 */
final class MethodReferenceCollectExample {
    public static void test() {
        List<String> lst = List.of("a", "ab", "bb", "ba", "cab", "ab", "a", "b", "cab", "dab");
        Set<String> doubled = lst.stream().parallel().filter(s -> s.length() <= 2)
            .collect(
                HashSet<String>::new,    // () -> new HashSet<String>()
                HashSet<String>::add,    // (HashSet<String> set, String s) -> set.add(s)
                HashSet<String>::addAll  // (HashSet<String> setA, HashSet<String> setB) -> setA.addAll(setB)  
            );
        for (var s : doubled)
            System.out.println(s);
    }
}

/**
 * Для комбинирования можно использовать не только коллекции.
 * Соберём строк через запятую используя StringBuilder.
 */
final class StringBuilderCollectExample {
    public static void test() {
        List<String> lst = List.of("Get", "Set", "Let", "Bad", "Cat", "Eat", "Rat");
        var res = lst.stream().parallel().collect(
            StringBuilder::new, // () -> new StringBuilder()
            (sb, s) -> sb.append(", ").append(s),
            StringBuilder::append // (StringBuilder sb1, StringBuilder sb2) -> sb1.append(sb2)
        ).substring(2);
        System.out.println(res);
    }
}

/**
 * Вариант (2) для метода collect позволяет передать только один объект, который самостоятельно предоставит все 
 * операции для сборки коллекции. Этот объект должен реализовывать сложный интерфейс Collector<T,A,R>. Интерфейс 
 * предполагает, что сначала данные будут собраны в промежуточную коллекцию, а потом преобразованы в результат.
 * Методы:
 *     Supplier<A>       supplier()    - предоставляет генератор для пустых коллекций
 *     BiConsumer<A,T>   accumulator() - предоставляет комбинатор коллекции и элемента потока данных
 *     BinaryOperator<A> combiner()    - предоставляет комбинатор, что из двух коллекций одинакового типа делает одну
 *     Function<A, R>    finisher()    - предоставляет функцию, которая из A делает R
 * 
 *     Set<Collector.Characteristics>	characteristics() - возвращает набор характеристик коллектора:
 *         является ли A Concurrent? Collector.Characteristics.CONCURRENT
 *         является ли А упорядоченным? Collector.Characteristics.UNORDERED
 *         является ли функция finisher() indentity? Collector.Characteristics.IDENTITY_FINISH
 * 
 * Главным образом этот интерфейс необходим стандартной библиотеке Java для создания удобных способов сбора элементов
 * в коллекции. Методы получения нужных коллекторов есть в классе Collectors.
 */

 /**
  * Собираем удвоенные строки в список.
  */
final class ListCollectorExample {
    public static void test() {
        List<String> lst = List.of("a", "ab", "bb", "ba", "cab", "dab");
        List<String> doubled = lst.stream().parallel().map(s -> s + s)
            .collect(
                Collectors.toList()   
            );
        for (var s : doubled)
            System.out.println(s);
    }
}

/**
 * Удобной являетя возможность группировать элементы по определённому признаку в Map.
 * Три метода Collectors:
 *     1) Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
 *     2) Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
 *     3) Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
 */

/**
 * (1) наиболее простой вариант. Он предоставляет Collector, который группирует элементы потока данных в списки по
 * переданному признаку и формирует HashMap.
 * Сформируем отображение из длины строки в набор строк.
 */
final class SimpleMapCollectorExample {
    public static void test() {
        List<String> lst = List.of("a", "bb", "b", "cab", "c", "db", "lab");
        Map<Integer, List<String>> map =
        lst.stream().parallel().collect(
            Collectors.groupingBy(String::length) // отображаем значение в ключ: (String s) -> s.length()
        );
        for (var key : map.keySet()) {
            System.out.format("key: %d\n", key);
            for (var string : map.get(key))
                System.out.format("    %s\n", string);
        }
    }
}

/**
 * (2) позволяет собрать не только в список, но и в любую коллекцию, например в Set.
 * Сформируем отображение из длин строк в уникальные наборы строк.
 */
final class ToCollectionMapCollectorExample {
    public static void test() {
        List<String> lst = List.of("a", "bb", "a", "cab", "c", "db", "cab", "bb", "c");
        Map<Integer, Set<String>> map =
        lst.stream().parallel().collect(
            Collectors.groupingBy(String::length, Collectors.toSet()) // добавили коллектор, чтобы собирать значения для ключа в определённую коллекцию
        );
        for (var key : map.keySet()) {
            System.out.format("key: %d\n", key);
            for (var string : map.get(key))
                System.out.format("    %s\n", string);
        }
    }
}

/**
 * (3) позволяет собрать именно в тот Map, который вы сами ожидаете.
 */
final class ToSortedMapCollectorExample {
    public static void test() {
        List<String> lst = List.of("a", "bb", "a", "cab", "c", "db", "cab", "bb", "c");
        SortedMap<Integer, Set<String>> map =
        lst.stream().parallel().collect(
            Collectors.groupingBy(String::length, TreeMap<Integer, Set<String>>::new, Collectors.toSet()) 
        );
        for (var key : map.keySet()) {
            System.out.format("key: %d\n", key);
            for (var string : map.get(key))
                System.out.format("    %s\n", string);
        }
    }
}

public class TerminalOperations {
    public static void main(String[] args) {
        if (false) ConcatReduceExample.test();
        if (false) AcronymExample.test();
        if (false) MaxLengthExample.test();
        if (false) MaxLengthMapReduceExample.test();
        if (false) ArrayListCollectExample.test();
        if (false) MethodReferenceCollectExample.test();
        if (false) StringBuilderCollectExample.test();
        if (false) ListCollectorExample.test();
        if (false) SimpleMapCollectorExample.test();
        if (false) ToCollectionMapCollectorExample.test();
        if (false) ToSortedMapCollectorExample.test();
    }
}
